1   package org.csstudio.swt.xygraph.linearscale;
2   
3   import java.math.BigDecimal;
4   import java.util.ArrayList;
5   import java.util.Calendar;
6   import java.util.Date;
7   
8   import org.eclipse.draw2d.Figure;
9   import org.eclipse.draw2d.FigureUtilities;
10  import org.eclipse.draw2d.Graphics;
11  import org.eclipse.draw2d.geometry.Dimension;
12  /**
13   * Linear Scale tick labels.
14   * @author Xihui Chen
15   */
16  public class LinearScaleTickLabels extends Figure {
17  
18      private static final int TICK_LABEL_GAP = 20; // Edited by scouter.project@gmail.com 8 -> 20
19  
20  	/** the array of tick label vales */
21      private ArrayList<Double> tickLabelValues;
22  
23      /** the array of tick label */
24      private ArrayList<String> tickLabels;
25  
26      /** the array of tick label position in pixels */
27      private ArrayList<Integer> tickLabelPositions;
28  
29      /** the array of visibility state of tick label */
30      private ArrayList<Boolean> tickVisibilities;
31  
32     
33  
34  	/** the maximum length of tick labels */
35      private int tickLabelMaxLength;
36      
37      /** the maximum height of tick labels */
38      private int tickLabelMaxHeight;
39      
40      private int gridStepInPixel;
41      
42      private LinearScale scale;
43  
44      /**
45       * Constructor.
46       * 
47       * @param linearScale
48       *            the scale
49       */
50      protected LinearScaleTickLabels(LinearScale linearScale) {
51      	
52      	this.scale = linearScale;
53          tickLabelValues = new ArrayList<Double>();
54          tickLabels = new ArrayList<String>();
55          tickLabelPositions = new ArrayList<Integer>();
56          tickVisibilities = new ArrayList<Boolean>();
57  
58          setFont(this.scale.getFont());
59          setForegroundColor(this.scale.getForegroundColor());
60      }
61  
62      /**
63       * Updates the tick labels.
64       * 
65       * @param length
66       *            scale tick length (without margin)
67       */
68      protected void update(int length) {
69          tickLabelValues.clear();
70          tickLabels.clear();
71          tickLabelPositions.clear();
72  
73  
74          if (scale.isLogScaleEnabled()) {
75              updateTickLabelForLogScale(length);
76          }else {
77              updateTickLabelForLinearScale(length);
78          }
79  
80          updateTickVisibility();
81          updateTickLabelMaxLengthAndHeight();
82      }
83  
84      /**
85       * Updates tick label for log scale.
86       * 
87       * @param length
88       *            the length of scale
89       */
90      private void updateTickLabelForLogScale(int length) {
91          double min = scale.getRange().getLower();
92          double max = scale.getRange().getUpper();
93          if(min <= 0 || max <= 0)
94          	throw new IllegalArgumentException(
95          			"the range for log scale must be in positive range");
96          boolean minBigger = max < min;
97  //        if (min >= max) {        	
98  //        	throw new IllegalArgumentException("min must be less than max.");
99  //        }
100         
101         int digitMin = (int) Math.ceil(Math.log10(min));
102         int digitMax = (int) Math.ceil(Math.log10(max));
103 
104         final BigDecimal MIN = new BigDecimal(new Double(min).toString());
105         BigDecimal tickStep = pow(10, digitMin - 1);
106         BigDecimal firstPosition;
107 
108         
109         if (MIN.remainder(tickStep).doubleValue() <= 0) {
110             firstPosition = MIN.subtract(MIN.remainder(tickStep));
111         } else {
112         	if(minBigger)
113         		firstPosition = MIN.subtract(MIN.remainder(tickStep));
114         	else
115         		firstPosition = MIN.subtract(MIN.remainder(tickStep)).add(tickStep);
116         }
117         
118       //add min
119        boolean minDateAdded =false;
120         if(MIN.compareTo(firstPosition) == (minBigger? 1:-1) ) {
121         	tickLabelValues.add(min);
122         	if (scale.isDateEnabled()) {
123                 Date date = new Date((long) MIN.doubleValue());
124                 tickLabels.add(scale.format(date, true));
125                 minDateAdded = true;
126             } else {
127                 tickLabels.add(scale.format(MIN.doubleValue()));
128             }
129         	tickLabelPositions.add(scale.getMargin());        	
130         }
131        
132         for (int i = digitMin; minBigger? i>=digitMax : i <=digitMax; i+=minBigger?-1:1) {        	
133         	 if(Math.abs(digitMax - digitMin) > 20){//if the range is too big, skip minor ticks.
134         		 BigDecimal v = pow(10,i);
135         		 if(v.doubleValue() > max)
136         			 break;
137         		 if (scale.isDateEnabled()) {
138 	                    Date date = new Date((long) v.doubleValue());
139 	                    tickLabels.add(scale.format(date, i==digitMin && !minDateAdded));
140 	                } else {
141 	                    tickLabels.add(scale.format(v.doubleValue()));
142 	                }
143 	                tickLabelValues.add(v.doubleValue());
144 	
145 	                int tickLabelPosition = (int) ((Math.log10(v.doubleValue()) - Math
146 	                        .log10(min))
147 	                        / (Math.log10(max) - Math.log10(min)) * length)
148 	                        + scale.getMargin();
149 	                tickLabelPositions.add(tickLabelPosition);     
150         	 }else{
151 	         	for (BigDecimal j = firstPosition; minBigger? j.doubleValue() >= pow(10, i-1)
152 	                    .doubleValue() : j.doubleValue() <= pow(10, i).doubleValue(); j = minBigger? j.subtract(tickStep) : j.add(tickStep)) {
153 	                if (minBigger? j.doubleValue() < max : j.doubleValue() > max) {
154 	                    break;
155 	                }
156 	
157 	                if (scale.isDateEnabled()) {
158 	                    Date date = new Date((long) j.doubleValue());
159 	                    tickLabels.add(scale.format(date, j==firstPosition && !minDateAdded));
160 	                } else {
161 	                    tickLabels.add(scale.format(j.doubleValue()));
162 	                }
163 	                tickLabelValues.add(j.doubleValue());
164 	
165 	                int tickLabelPosition = (int) ((Math.log10(j.doubleValue()) - Math
166 	                        .log10(min))
167 	                        / (Math.log10(max) - Math.log10(min)) * length)
168 	                        + scale.getMargin();
169 	                tickLabelPositions.add(tickLabelPosition);               
170 	            }
171 	         	tickStep = minBigger? tickStep.divide(pow(10,1)) : tickStep.multiply(pow(10, 1));
172 	            firstPosition = minBigger? pow(10,i-1) : tickStep.add(pow(10, i));
173          	}           
174         }
175         
176         //add max
177         if(minBigger? max < tickLabelValues.get(tickLabelValues.size()-1) 
178         		: max > tickLabelValues.get(tickLabelValues.size()-1)) {
179         	tickLabelValues.add(max);
180         	if (scale.isDateEnabled()) {
181                 Date date = new Date((long) max);
182                 tickLabels.add(scale.format(date, true));
183             } else {
184                 tickLabels.add(scale.format(max));
185             }
186         	tickLabelPositions.add(scale.getMargin() + length);
187         }
188     }
189 
190    
191 
192      /**
193      * Updates tick label for normal scale.
194      * 
195      * @param length
196      *            scale tick length (without margin)
197      */
198     private void updateTickLabelForLinearScale(int length) {
199         double min = scale.getRange().getLower();
200         double max = scale.getRange().getUpper();
201         BigDecimal gridStepBigDecimal = getGridStep(length, min, max);
202         gridStepInPixel = (int) (length * gridStepBigDecimal.doubleValue()/(max - min));
203         updateTickLabelForLinearScale(length, gridStepBigDecimal);
204     }
205 
206     /**
207      * Updates tick label for normal scale.
208      * 
209      * @param length
210      *            scale tick length (without margin)
211      * @param tickStep
212      *            the tick step
213      */
214     private void updateTickLabelForLinearScale(int length, BigDecimal tickStep) {
215         double min = scale.getRange().getLower();
216         double max = scale.getRange().getUpper();
217 
218         boolean minBigger = max < min;
219         
220         final BigDecimal MIN = new BigDecimal(new Double(min).toString());
221         BigDecimal firstPosition;
222 
223         //make firstPosition as the right most of min based on tickStep 
224         /* if (min % tickStep <= 0) */
225         if (MIN.remainder(tickStep).doubleValue() <= 0) {
226             /* firstPosition = min - min % tickStep */
227             firstPosition = MIN.subtract(MIN.remainder(tickStep));
228         } else {
229             /* firstPosition = min - min % tickStep + tickStep */
230             firstPosition = MIN.subtract(MIN.remainder(tickStep)).add(tickStep);
231         }
232 
233         // the unit time starts from 1:00
234         if (scale.isDateEnabled()) {
235             BigDecimal zeroOclock = firstPosition.subtract(new BigDecimal(
236                     new Double(3600000).toString()));
237             if (MIN.compareTo(zeroOclock) == -1) {
238                 firstPosition = zeroOclock;
239             }
240         }
241         
242         //add min
243         int r = minBigger? 1 : -1;
244         boolean minDateAdded = false;
245         if(MIN.compareTo(firstPosition) == r ) {
246         	tickLabelValues.add(min);
247         	if (scale.isDateEnabled()) {
248                 Date date = new Date((long) MIN.doubleValue());
249                 tickLabels.add(scale.format(date, true));
250                 minDateAdded = true;
251             } else {
252                 tickLabels.add(scale.format(MIN.doubleValue()));
253             }
254         	tickLabelPositions.add(scale.getMargin());        	
255         }
256         	
257         for (BigDecimal b = firstPosition; max >= min ? b.doubleValue() < max : b.doubleValue() >max; b = b
258                 .add(tickStep)) {
259             if (scale.isDateEnabled()) {
260                 Date date = new Date((long) b.doubleValue()); 
261                 tickLabels.add(scale.format(date, b==firstPosition && !minDateAdded));
262             } else {
263                 tickLabels.add(scale.format(b.doubleValue()));
264             }
265             tickLabelValues.add(b.doubleValue());
266 
267             int tickLabelPosition = (int) ((b.doubleValue() - min)
268                     / (max - min) * length) + scale.getMargin();
269                     //- LINE_WIDTH;
270             tickLabelPositions.add(tickLabelPosition);
271         }
272         
273         //always add max
274 //        if((minBigger ? max < tickLabelValues.get(tickLabelValues.size()-1) :
275 //        	max > tickLabelValues.get(tickLabelValues.size()-1) )) {
276         	tickLabelValues.add(max);
277         	if (scale.isDateEnabled()) {
278                 Date date = new Date((long) max);
279                 tickLabels.add(scale.format(date, true));
280             } else {
281                 tickLabels.add(scale.format(max));
282             }
283         	tickLabelPositions.add(scale.getMargin() + length);
284 //        }
285         	
286     }
287 
288     /**
289      * Updates the visibility of tick labels.
290      */
291     private void updateTickVisibility() {
292 
293         // initialize the array of tick label visibility state
294         tickVisibilities.clear();
295         for (int i = 0; i < tickLabelPositions.size(); i++) {
296             tickVisibilities.add(Boolean.TRUE);
297         }
298 
299         if (tickLabelPositions.size() == 0) {
300             return;
301         }
302 
303         // set the tick label visibility
304         int previousPosition = 0;
305         String previousLabel = null;
306         for (int i = 0; i < tickLabelPositions.size(); i++) {
307 
308             // check if there is enough space to draw tick label
309             boolean hasSpaceToDraw = true;
310             if (i != 0) {
311                 hasSpaceToDraw = hasSpaceToDraw(previousPosition,
312                         tickLabelPositions.get(i), previousLabel, tickLabels.get(i));
313             }
314 
315             // check if the same tick label is repeated
316             String currentLabel = tickLabels.get(i);
317             boolean isRepeatSameTickAndNotEnd = currentLabel.equals(previousLabel) &&
318             	(i!=0 && i!=tickLabelPositions.size()-1);
319             
320             // check if the tick label value is major
321             boolean isMajorTickOrEnd = true;
322             if (scale.isLogScaleEnabled()) {
323                 isMajorTickOrEnd = isMajorTick(tickLabelValues.get(i)) 
324                 	|| i==0 || i==tickLabelPositions.size()-1;
325             }
326 
327             if (!hasSpaceToDraw || isRepeatSameTickAndNotEnd || !isMajorTickOrEnd) {
328                 tickVisibilities.set(i, Boolean.FALSE);
329             } else {
330                 previousPosition = tickLabelPositions.get(i);
331                 previousLabel = currentLabel;
332             }
333         }
334     }
335 
336 
337     /**
338      * Checks if the tick label is major (...,0.01,0.1,1,10,100,...).
339      * 
340      * @param tickValue
341      *            the tick label value
342      * @return true if the tick label is major
343      */
344     private boolean isMajorTick(double tickValue) {
345         if (!scale.isLogScaleEnabled()) {
346             return true;
347         }
348 
349         if (Math.log10(tickValue) % 1 == 0) {
350             return true;
351         }
352 
353         return false;
354     }
355 
356     /**
357      * Returns the state indicating if there is a space to draw tick label.
358      * 
359      * @param previousPosition
360      *            the previously drawn tick label position.
361      * @param tickLabelPosition
362      *            the tick label position.
363      *  @param previousTickLabel
364      *            the prevoius tick label.          
365      * @param tickLabel
366      *            the tick label text
367      * @return true if there is a space to draw tick label
368      */
369     private boolean hasSpaceToDraw(int previousPosition, int tickLabelPosition,
370             String previousTickLabel, String tickLabel) {
371         Dimension tickLabelSize = FigureUtilities.getTextExtents(tickLabel, scale.getFont());
372         Dimension previousTickLabelSize = FigureUtilities.getTextExtents(previousTickLabel, scale.getFont());
373         int interval = tickLabelPosition - previousPosition;
374         int textLength = (int) (scale.isHorizontal() ? (tickLabelSize.width/2.0 + previousTickLabelSize.width/2.0)  
375         		: tickLabelSize.height);
376         boolean noLapOnPrevoius = true;
377        
378         boolean noLapOnEnd = true;
379         if(tickLabelPosition != tickLabelPositions.get(tickLabelPositions.size() - 1)){ //if it is not the end tick label.
380         	noLapOnPrevoius = interval > (textLength+TICK_LABEL_GAP);
381         	Dimension endTickLabelSize = FigureUtilities.getTextExtents(
382         		tickLabels.get(tickLabels.size()-1), scale.getFont());
383         	interval = tickLabelPositions.get(tickLabelPositions.size() - 1) - tickLabelPosition;
384         	textLength = (int) (scale.isHorizontal() ? (tickLabelSize.width/2.0 + endTickLabelSize.width/2.0)
385         			: tickLabelSize.height);
386         	noLapOnEnd = interval > textLength+TICK_LABEL_GAP;
387         }       
388         return noLapOnPrevoius && noLapOnEnd;        
389     }
390 
391     /**
392      * Gets max length of tick label.
393      */
394     private void updateTickLabelMaxLengthAndHeight() {
395         int maxLength = 0;
396         int maxHeight = 0; 
397         for (int i = 0; i < tickLabels.size(); i++) {
398             if (tickVisibilities.size() > i && tickVisibilities.get(i) == true) {
399             	Dimension p = FigureUtilities.getTextExtents(tickLabels.get(i), scale.getFont());
400             	if (tickLabels.get(0).startsWith("-") && !tickLabels.get(i).startsWith("-")) {
401                     p.width += FigureUtilities.getTextExtents("-", getFont()).width;
402                 }
403                 if (p.width > maxLength) {
404                     maxLength = p.width;
405                 }
406                 if(p.height > maxHeight){
407                 	maxHeight = p.height;
408                 }
409             }
410         }
411         tickLabelMaxLength = maxLength;
412         tickLabelMaxHeight = maxHeight;
413     }
414 
415     /**
416      * Calculates the value of the first argument raised to the power of the
417      * second argument.
418      * 
419      * @param base
420      *            the base
421      * @param expornent
422      *            the exponent
423      * @return the value <tt>a<sup>b</sup></tt> in <tt>BigDecimal</tt>
424      */
425     private BigDecimal pow(double base, int expornent) {
426         BigDecimal value;
427         if (expornent > 0) {
428             value = new BigDecimal(new Double(base).toString()).pow(expornent);
429         } else {
430             value = BigDecimal.ONE.divide(new BigDecimal(new Double(base)
431                     .toString()).pow(-expornent));
432         }
433         return value;
434     }
435 
436     
437     
438     /**
439      * Gets the grid step.
440      * 
441      * @param lengthInPixels
442      *            scale length in pixels
443      * @param min
444      *            minimum value
445      * @param max
446      *            maximum value
447      * @return rounded value.
448      */
449     private BigDecimal getGridStep(int lengthInPixels, double min, double max) {
450     	if((int) scale.getMajorGridStep() != 0) {
451     		return new BigDecimal(scale.getMajorGridStep());
452     	}
453     	
454         if (lengthInPixels <= 0) {
455             lengthInPixels = 1;
456         }
457         boolean minBigger = false;
458         if (min >= max) {        	
459         	if(max == min)
460         		max ++;
461         	else{
462         		minBigger = true;
463         		double swap = min;
464         		min = max;
465         		max= swap;
466         	}
467 //        		throw new IllegalArgumentException("min must be less than max.");
468         }
469 
470         double length = Math.abs(max - min);
471         double majorTickMarkStepHint = scale.getMajorTickMarkStepHint();
472         if(majorTickMarkStepHint > lengthInPixels)
473         	majorTickMarkStepHint = lengthInPixels;
474 //        if(min > max)
475 //        	majorTickMarkStepHint = -majorTickMarkStepHint;        
476         double gridStepHint = length / lengthInPixels
477                 * majorTickMarkStepHint;
478         
479         
480         if(scale.isDateEnabled()) {
481         	//by default, make the least step to be minutes
482         	
483         	long timeStep;
484         	if(max-min<10000) //<10 sec, step = 1 ms
485         		timeStep=1l;
486         	else if(max - min < 60000) // < 1 min, step = 1 sec
487         		timeStep = 1000l;
488         	else if(max - min < 600000) // < 10 min, step = 10 sec
489         		timeStep= 10000l;
490         	else if (max -min < 6400000) // < 2 hour, step = 1 min
491         		timeStep = 60000l;
492         	else if (max -min < 43200000) // < 12 hour, step = 10 min
493         		timeStep = 600000l;
494         	else if (max -min < 86400000) // < 24 hour, step = 30 min
495         		timeStep = 1800000l;
496         	else if (max - min < 604800000) // < 7 days, step = 1 hour
497         		timeStep = 3600000l;
498         	else 
499         		timeStep = 86400000l;
500         		
501         	if (scale.getTimeUnit() == Calendar.SECOND) {
502         		timeStep = 1000l;
503         	} else if (scale.getTimeUnit() == Calendar.MINUTE) {
504         		timeStep = 60000l;
505         	}else if (scale.getTimeUnit() == Calendar.HOUR_OF_DAY) {
506         		timeStep = 3600000l;
507         	}else if (scale.getTimeUnit() == Calendar.DATE) {
508         		timeStep = 86400000l;
509         	}else if (scale.getTimeUnit() == Calendar.MONTH) {
510         		timeStep = 30l*86400000l;
511         	}else if (scale.getTimeUnit() == Calendar.YEAR) {
512         		timeStep = 365l*86400000l;  
513         	}
514         	double temp = gridStepHint + (timeStep - gridStepHint%timeStep);       	
515         	if(minBigger)
516         		temp = -temp;
517         	return new BigDecimal(temp);
518         }
519         	
520         
521         double mantissa = gridStepHint;
522         int exponent = 0;
523         if (mantissa < 1) {
524         	if(mantissa != 0)
525 	            while (mantissa < 1) {
526 	                mantissa *= 10.0;
527 	                exponent--;
528 	            }
529         } else {
530             while (mantissa >= 10) {
531                 mantissa /= 10.0;
532                 exponent++;
533             }
534         }
535 
536         BigDecimal gridStep;
537         if (mantissa > 7.5) {            
538             gridStep = BigDecimal.TEN.multiply(pow(10, exponent)); // 10.0 * 10 ** exponent 
539         } else if (mantissa > 3.5) {           
540             gridStep = new BigDecimal(new Double(5).toString()).multiply(pow(  // 5.0 * 10 ** exponent
541                     10, exponent));
542         } else if (mantissa > 1.5) {            
543             gridStep = new BigDecimal(new Double(2).toString()).multiply(pow( // 2.0 * 10 ** exponent
544                     10, exponent));
545         } else {            
546             gridStep = pow(10, exponent); // 1.0 * 10 ** exponent
547         }
548         if(minBigger)
549         	gridStep = gridStep.negate();
550         return gridStep;
551     }
552 
553     /**
554      * Gets the tick label positions.
555      * 
556      * @return the tick label positions
557      */
558     public ArrayList<Integer> getTickLabelPositions() {
559         return tickLabelPositions;
560     }
561 
562     @Override
563     protected void paintClientArea(Graphics graphics) {
564     	graphics.translate(bounds.x, bounds.y);
565     	if (scale.isHorizontal()) {
566             drawXTick(graphics);
567         } else {
568             drawYTick(graphics);
569         }
570 
571     	super.paintClientArea(graphics);
572     };
573 
574     /**
575      * Draw the X tick.
576      * 
577      * @param grahics
578      *            the graphics context
579      */
580     private void drawXTick(Graphics grahics) {
581         // draw tick labels
582         grahics.setFont(scale.getFont());
583         for (int i = 0; i < tickLabelPositions.size(); i++) {
584             if (tickVisibilities.get(i) == true) {
585                 String text = tickLabels.get(i);
586                 int fontWidth = FigureUtilities.getTextExtents(text, getFont()).width;
587                 int x = (int) Math.ceil(tickLabelPositions.get(i) - fontWidth / 2.0);// + offset);
588                 grahics.drawText(text, x, 0);
589             }
590         }
591     }
592 
593     /**
594      * Draw the Y tick.
595      * 
596      * @param grahpics
597      *            the graphics context
598      */
599     private void drawYTick(Graphics grahpics) {
600         // draw tick labels
601         grahpics.setFont(scale.getFont());
602         int fontHeight = tickLabelMaxHeight;
603         for (int i = 0; i < tickLabelPositions.size(); i++) {
604             if (tickVisibilities.size() == 0 || tickLabels.size() == 0) {
605                 break;
606             }
607 
608             if (tickVisibilities.get(i) == true) {
609                 String text = tickLabels.get(i);
610                 int x = 0;
611                 if (tickLabels.get(0).startsWith("-") && !text.startsWith("-")) {
612                     x += FigureUtilities.getTextExtents("-", getFont()).width;
613                 }
614                 int y = (int) Math.ceil(scale.getLength() - tickLabelPositions.get(i)
615                         - fontHeight / 2.0);
616                 grahpics.drawText(text, x, y);
617             }
618         }
619     }
620 
621 	/**
622 	 * @return the tickLabelMaxLength
623 	 */
624 	public int getTickLabelMaxLength() {
625 		return tickLabelMaxLength;
626 	}
627 
628 	/**
629 	 * @return the tickLabelMaxHeight
630 	 */
631 	public int getTickLabelMaxHeight() {
632 		return tickLabelMaxHeight;
633 	}
634 
635 	 /**
636 	 * @return the tickVisibilities
637 	 */
638 	public ArrayList<Boolean> getTickVisibilities() {
639 		return tickVisibilities;
640 	}
641 	
642 	/**
643 	 * @return the gridStepInPixel
644 	 */
645 	public int getGridStepInPixel() {
646 		return gridStepInPixel;
647 	}
648 
649 }